Dybdegående guide til JavaScript Decorators. Udforsk syntaks, metadata-programmering, bedste praksis og kodens vedligeholdelighed med praktiske eksempler.
JavaScript Decorators: Implementering af Metadata-programmering
JavaScript Decorators er en kraftfuld funktion, der giver dig mulighed for at tilføje metadata og ændre adfærden for klasser, metoder, egenskaber og parametre på en deklarativ og genanvendelig måde. De er et 'stage 3'-forslag i ECMAScript-standardprocessen og bruges i vid udstrækning med TypeScript, som har sin egen (lidt anderledes) implementering. Denne artikel giver et omfattende overblik over JavaScript Decorators med fokus på deres rolle i metadata-programmering og illustrerer deres anvendelse med praktiske eksempler.
Hvad er JavaScript Decorators?
Decorators er et designmønster, der forbedrer eller ændrer funktionaliteten af et objekt uden at ændre dets struktur. I JavaScript er decorators specielle slags erklæringer, der kan knyttes til klasser, metoder, accessors, egenskaber eller parametre. De bruger @-symbolet efterfulgt af en funktion, der vil blive udført, når det dekorerede element defineres.
Tænk på decorators som funktioner, der tager det dekorerede element som input og returnerer en ændret version af det element, eller udfører en sideeffekt baseret på det. Dette giver en ren og elegant måde at tilføje funktionalitet på uden at ændre den oprindelige klasse eller funktion direkte.
Nøglebegreber:
- Decorator-funktion: Funktionen, der er forudgået af
@-symbolet. Den modtager information om det dekorerede element og kan ændre det. - Dekoreret element: Klassen, metoden, accessoren, egenskaben eller parameteren, der er dekoreret.
- Metadata: Data, der beskriver data. Decorators bruges ofte til at associere metadata med kodeelementer.
Syntaks og struktur
Den grundlæggende syntaks for en decorator er som følger:
@decorator
class MyClass {
// Klassemedlemmer
}
Her er @decorator decorator-funktionen, og MyClass er den dekorerede klasse. Decorator-funktionen kaldes, når klassen defineres, og kan tilgå og ændre klassedefinitionen.
Decorators kan også acceptere argumenter, som overføres til selve decorator-funktionen:
@loggable(true, "Custom Message")
class MyClass {
// Klassemedlemmer
}
I dette tilfælde er loggable en 'decorator factory'-funktion, som tager argumenter og returnerer den faktiske decorator-funktion. Dette giver mulighed for mere fleksible og konfigurerbare decorators.
Typer af Decorators
Der findes forskellige typer af decorators, afhængigt af hvad de dekorerer:
- Klasse-decorators: Anvendes på klasser.
- Metode-decorators: Anvendes på metoder i en klasse.
- Accessor-decorators: Anvendes på getter- og setter-accessors.
- Egenskabs-decorators: Anvendes på klasseegenskaber.
- Parameter-decorators: Anvendes på parametre i en metode.
Klasse-decorators
Klasse-decorators bruges til at ændre eller forbedre adfærden af en klasse. De modtager klassens constructor som et argument og kan returnere en ny constructor til at erstatte den oprindelige. Dette gør det muligt at tilføje funktionalitet som logging, dependency injection eller state management.
Eksempel:
function loggable(constructor: Function) {
console.log("Klasse " + constructor.name + " blev oprettet.");
}
@loggable
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
const user = new User("Alice"); // Outputs: Klasse User blev oprettet.
I dette eksempel logger loggable-decoratoren en besked til konsollen, hver gang en ny instans af User-klassen oprettes. Dette kan være nyttigt til fejlfinding eller overvågning.
Metode-decorators
Metode-decorators bruges til at ændre adfærden af en metode i en klasse. De modtager følgende argumenter:
target: Prototypen for klassen.propertyKey: Navnet på metoden.descriptor: Egenskabsdescriptoren for metoden.
Descriptoren giver dig mulighed for at tilgå og ændre metodens adfærd, såsom at omkranse den med yderligere logik eller omdefinere den fuldstændigt.
Eksempel:
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Kalder metoden ${propertyKey} med argumenter: ${args}`);
const result = originalMethod.apply(this, args);
console.log(`Metoden ${propertyKey} returnerede: ${result}`);
return result;
};
return descriptor;
}
class Calculator {
@logMethod
add(a: number, b: number): number {
return a + b;
}
}
const calculator = new Calculator();
const sum = calculator.add(5, 3); // Outputs logger for metodekaldet og returværdien
I dette eksempel logger logMethod-decoratoren metodens argumenter og returværdi. Dette kan være nyttigt til fejlfinding og ydelsesovervågning.
Accessor-decorators
Accessor-decorators ligner metode-decorators, men anvendes på getter- og setter-accessors. De modtager de samme argumenter som metode-decorators og giver dig mulighed for at ændre accessorens adfærd.
Eksempel:
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSet = descriptor.set;
descriptor.set = function (value: any) {
if (value < 0) {
throw new Error("Værdien skal være ikke-negativ.");
}
originalSet.call(this, value);
};
}
class Temperature {
private _celsius: number;
constructor(celsius: number) {
this._celsius = celsius;
}
@validate
set celsius(value: number) {
this._celsius = value;
}
get celsius(): number {
return this._celsius;
}
}
const temperature = new Temperature(25);
temperature.celsius = 30; // Gyldig
// temperature.celsius = -10; // Kaster en fejl
I dette eksempel sikrer validate-decoratoren, at temperaturværdien ikke er negativ. Dette kan være nyttigt til at håndhæve dataintegritet.
Egenskabs-decorators
Egenskabs-decorators bruges til at ændre adfærden af en klasseegenskab. De modtager følgende argumenter:
target: Prototypen for klassen (for instansegenskaber) eller klassens constructor (for statiske egenskaber).propertyKey: Navnet på egenskaben.
Egenskabs-decorators kan bruges til at definere metadata eller ændre egenskabens descriptor.
Eksempel:
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false,
});
}
class Configuration {
@readonly
apiUrl: string = "https://api.example.com";
}
const config = new Configuration();
// config.apiUrl = "https://newapi.example.com"; // Kaster en fejl i strict mode
I dette eksempel gør readonly-decoratoren apiUrl-egenskaben skrivebeskyttet, hvilket forhindrer den i at blive ændret efter initialisering. Dette kan være nyttigt til at definere uforanderlige konfigurationsværdier.
Parameter-decorators
Parameter-decorators bruges til at ændre adfærden af en metodeparameter. De modtager følgende argumenter:
target: Prototypen for klassen (for instansmetoder) eller klassens constructor (for statiske metoder).propertyKey: Navnet på metoden.parameterIndex: Parameterens indeks i metodens parameterliste.
Parameter-decorators bruges mindre hyppigt end andre typer decorators, men de kan være nyttige til at validere inputparametre eller til dependency injection.
Eksempel:
function required(target: any, propertyKey: string, parameterIndex: number) {
const existingRequiredParameters: number[] = Reflect.getOwnMetadata(propertyKey, target, "required") || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(propertyKey, existingRequiredParameters, target, "required");
}
function validateMethod(target: any, propertyName: string, descriptor: PropertyDescriptor) {
let method = descriptor.value!;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(propertyName, target, "required");
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (arguments[parameterIndex] === null || arguments[parameterIndex] === undefined) {
throw new Error(`Manglende påkrævet argument på indeks ${parameterIndex}`);
}
}
}
return method.apply(this, arguments);
};
}
class ArticleService {
create(
@required title: string,
@required content: string
): void {
console.log(`Opretter artikel med titel: ${title} og indhold: ${content}`);
}
}
const service = new ArticleService();
// service.create("My Article", null); // Kaster en fejl
service.create("Min artikel", "Artikelindhold"); // Gyldig
I dette eksempel markerer required-decoratoren parametre som påkrævede, og validateMethod-decoratoren sikrer, at disse parametre ikke er null eller undefined. Dette kan være nyttigt til at håndhæve validering af metodeinput.
Metadata-programmering med Decorators
En af de mest kraftfulde anvendelser af decorators er metadata-programmering. Metadata er data om data. I programmeringssammenhæng er det data, der beskriver din kodes struktur, adfærd og formål. Decorators giver en ren og deklarativ måde at associere metadata med klasser, metoder, egenskaber og parametre.
Reflect Metadata API'et
Reflect Metadata API'et er et standard-API, der giver dig mulighed for at gemme og hente metadata, der er associeret med objekter. Det tilbyder følgende funktioner:
Reflect.defineMetadata(key, value, target, propertyKey): Definerer metadata for en specifik egenskab på et objekt.Reflect.getMetadata(key, target, propertyKey): Henter metadata for en specifik egenskab på et objekt.Reflect.hasMetadata(key, target, propertyKey): Tjekker, om der findes metadata for en specifik egenskab på et objekt.Reflect.deleteMetadata(key, target, propertyKey): Sletter metadata for en specifik egenskab på et objekt.
Du kan bruge disse funktioner i kombination med decorators for at associere metadata med dine kodeelementer.
Eksempel: Definition og hentning af metadata
import 'reflect-metadata';
const logKey = "log";
function log(message: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
Reflect.defineMetadata(logKey, message, target, key);
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(Reflect.getMetadata(logKey, target, key));
const result = originalMethod.apply(this, args);
return result;
}
return descriptor;
}
}
class Example {
@log("Udfører metode")
myMethod(arg: string): string {
return `Metode kaldt med ${arg}`;
}
}
const example = new Example();
example.myMethod("Hej"); // Outputs: Udfører metode, Metode kaldt med Hej
I dette eksempel bruger log-decoratoren Reflect Metadata API'et til at associere en logbesked med myMethod-metoden. Når metoden kaldes, henter og logger decoratoren beskeden til konsollen.
Anvendelsestilfælde for Metadata-programmering
Metadata-programmering med decorators har mange praktiske anvendelser, herunder:
- Serialisering og deserialisering: Annotér egenskaber med metadata for at styre, hvordan de serialiseres eller deserialiseres til/fra JSON eller andre formater. Dette kan være nyttigt, når man håndterer data fra eksterne API'er eller databaser, især i distribuerede systemer, der kræver datatransformation på tværs af forskellige platforme (f.eks. konvertering af datoformater mellem forskellige regionale standarder). Forestil dig en e-handelsplatform, der håndterer internationale leveringsadresser, hvor du kan bruge metadata til at specificere det korrekte adresseformat og valideringsregler for hvert land.
- Dependency Injection: Brug metadata til at identificere afhængigheder, der skal injiceres i en klasse. Dette forenkler håndteringen af afhængigheder og fremmer løs kobling. Overvej en microservices-arkitektur, hvor services er afhængige af hinanden. Decorators og metadata kan facilitere den dynamiske injektion af service-klienter baseret på konfiguration, hvilket giver lettere skalering og fejltolerance.
- Validering: Definer valideringsregler som metadata og brug decorators til automatisk at validere data. Dette sikrer dataintegritet og reducerer boilerplate-kode. For eksempel skal en global finansiel applikation overholde forskellige regionale finansielle regulativer. Metadata kan definere valideringsregler for valutaformater, skatteberegninger og transaktionsgrænser baseret på brugerens placering, hvilket sikrer overholdelse af lokale love.
- Routing og Middleware: Brug metadata til at definere ruter og middleware for webapplikationer. Dette forenkler konfigurationen af din applikation og gør den mere vedligeholdelsesvenlig. Et globalt distribueret content delivery network (CDN) kan bruge metadata til at definere caching-politikker og routing-regler baseret på indholdstypen og brugerens placering, hvilket optimerer ydeevnen og reducerer latenstid for brugere verden over.
- Autorisation og autentificering: Associér roller, tilladelser og autentificeringskrav med metoder og klasser, hvilket faciliterer deklarative sikkerhedspolitikker. Forestil dig en multinational virksomhed med ansatte i forskellige afdelinger og lokationer. Decorators kan definere adgangskontrolregler baseret på brugerens rolle, afdeling og placering, hvilket sikrer, at kun autoriseret personale kan tilgå følsomme data og funktionaliteter.
Bedste praksis
Når du bruger JavaScript Decorators, bør du overveje følgende bedste praksis:
- Hold Decorators simple: Decorators bør være fokuserede og udføre en enkelt, veldefineret opgave. Undgå kompleks logik i decorators for at bevare læsbarhed og vedligeholdelighed.
- Brug Decorator Factories: Brug 'decorator factories' for at tillade konfigurerbare decorators. Dette gør dine decorators mere fleksible og genanvendelige.
- Undgå sideeffekter: Decorators bør primært fokusere på at ændre det dekorerede element eller associere metadata med det. Undgå at udføre komplekse sideeffekter i decorators, der kan gøre din kode sværere at forstå og fejlfinde.
- Brug TypeScript: TypeScript giver fremragende understøttelse for decorators, herunder type-tjek og IntelliSense. At bruge TypeScript kan hjælpe dig med at fange fejl tidligt og forbedre din udviklingsoplevelse.
- Dokumentér dine Decorators: Dokumentér dine decorators tydeligt for at forklare deres formål, og hvordan de skal bruges. Dette gør det lettere for andre udviklere at forstå og bruge dine decorators korrekt.
- Overvej ydeevne: Selvom decorators er kraftfulde, kan de også påvirke ydeevnen. Vær opmærksom på ydeevnekonsekvenserne af dine decorators, især i ydeevnekritiske applikationer.
Eksempler på internationalisering med Decorators
Decorators kan hjælpe med internationalisering (i18n) og lokalisering (l10n) ved at associere lokalespecifikke data og adfærd med kodekomponenter:
Eksempel: Lokaliseret datoformatering
import 'reflect-metadata';
interface DateFormatOptions {
locale: string;
options?: Intl.DateTimeFormatOptions;
}
const dateFormatKey = 'dateFormat';
function formatDate(options: DateFormatOptions) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata(dateFormatKey, options, target, propertyKey);
};
}
class Event {
@formatDate({ locale: 'fr-FR', options: { year: 'numeric', month: 'long', day: 'numeric' } })
startDate: Date;
constructor(startDate: Date) {
this.startDate = startDate;
}
getFormattedStartDate(): string {
const options: DateFormatOptions = Reflect.getMetadata(dateFormatKey, Object.getPrototypeOf(this), 'startDate');
return this.startDate.toLocaleDateString(options.locale, options.options);
}
}
const event = new Event(new Date());
console.log(event.getFormattedStartDate()); // Outputs dato i fransk format
Eksempel: Valutaformatering baseret på brugerens placering
import 'reflect-metadata';
interface CurrencyFormatOptions {
locale: string;
currency: string;
}
const currencyFormatKey = 'currencyFormat';
function formatCurrency(options: CurrencyFormatOptions) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata(currencyFormatKey, options, target, propertyKey);
};
}
class Product {
@formatCurrency({ locale: 'de-DE', currency: 'EUR' })
price: number;
constructor(price: number) {
this.price = price;
}
getFormattedPrice(): string {
const options: CurrencyFormatOptions = Reflect.getMetadata(currencyFormatKey, Object.getPrototypeOf(this), 'price');
return this.price.toLocaleString(options.locale, { style: 'currency', currency: options.currency });
}
}
const product = new Product(99.99);
console.log(product.getFormattedPrice()); // Outputs pris i tysk euroformat
Fremtidige overvejelser
JavaScript decorators er en funktion i udvikling, og standarden er stadig undervejs. Nogle fremtidige overvejelser inkluderer:
- Standardisering: ECMAScript-standarden for decorators er stadig i proces. Efterhånden som standarden udvikler sig, kan der komme ændringer i syntaksen og adfærden for decorators.
- Ydeevneoptimering: Efterhånden som decorators bliver mere udbredte, vil der være behov for ydeevneoptimeringer for at sikre, at de ikke påvirker applikationens ydeevne negativt.
- Værktøjsunderstøttelse: Forbedret værktøjsunderstøttelse for decorators, såsom IDE-integration og fejlfindingsværktøjer, vil gøre det lettere for udviklere at bruge decorators effektivt.
Konklusion
JavaScript Decorators er et kraftfuldt værktøj til at implementere metadata-programmering og forbedre adfærden af din kode. Ved at bruge decorators kan du tilføje funktionalitet på en ren, deklarativ og genanvendelig måde. Dette fører til mere vedligeholdelsesvenlig, testbar og skalerbar kode. At forstå de forskellige typer af decorators og hvordan man bruger dem effektivt er essentielt for moderne JavaScript-udvikling. Decorators, især når de kombineres med Reflect Metadata API'et, åbner op for en række muligheder, fra dependency injection og validering til serialisering og routing, hvilket gør din kode mere udtryksfuld og lettere at administrere.